8. Exceptions

 

 

·     Undantagshantering i C++.

 

·     Syntax.

 

·     Exempel.

 

·     Återställning.

 

·     Identifiering av undantagstyp.

 

·     Undantag som inte hanterats.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Undantagshantering i C++.

 

C++ stöder s.k. ‘exception handling’, d.v.s. undantagshantering. Detta innebär möjlighet att hantera exceptionella situationer, i regel fel av olika slag, på så sätt att man förpassar dessa till en högre nivå i systemets bearbetningshierarki, vil­ken bättre lämpar sig för felhantering. Hanteringen sker alltså utanför program­mets normala flöde.

 

Det finns motsvarande hantering i de flesta operativsystem, och den hanteringen är i regel bättre anpassad för operativsystemet i fråga, men användningen av han­tering som strukturerats efter operativsystemet förlorar i portatibilitet, d.v.s. blir svårare att flytta till en annan operativmiljö. Vi kommer i detta kapitel att hål­la oss till den hantering som är standard i C++. Det är ju det språket vi syss­lar med i detta häfte.

 

Hanteringen i C++ är i regel mer flexibel än den i C, på så sätt att den kan han­tera valfri typ av undantag. Den gamla hanteringen i C är alltid av typen ‘unsig­ned int’. I C++ kan man ‘märka’ ett undantag med valfri datatyp. I Såväl C som C++ är det möjligt, och även vanligt, att man skickar ett argument från undan­tags­tillfället (där felet upptäcks) till undantagshanteraren (där felet hanteras).

 

I C++ kallar man processen att ett fel utlöser felhanteringen för att den ‘kastar ut’ ett ‘undantag’ (‘throwing’ an ‘exception’). En härför avsedd undantags­han­terare (exception handler) ‘fångar in’ (‘catches’) undantaget. Man ‘kastar ut’ un­dantag i ett visst kodavsnitt, vilket står efter nyckelordet ‘try’ (försök). Efter detta kodavsnitt fångar man upp eventuella utkast i en ‘catch’-sats.

 

Finessen i C++ undantagshantering är att allt som gjorts i ‘try’-koden återställs automatiskt, d.v.s. blev det fel mitt i kodavsnittet kommer allt att vara som det var innan programmet började utföra det. Dessutom kan kodavsnittet innehålla anrop av funktioner som själv innehåller undantagshantering.

 

Det är inte tvunget att man fångar upp alla de undantag som kastats ut. Om det inte finns någon motsvarande ‘catch’, kan eventuellt anropande funktion ha en ‘catch’ som fångar upp undantaget. Skulle det inte alls fångas upp finns det för­definierad hantering, ‘terminate’, vilken avslutar programkörningen. Man kan definiera om ‘terminate’.

 

Man måste ange att man vill använda undantagshanteringen vid kompilering. En kompileringsswitch ‘/GX’ kan anges i ‘Project Settings’ under fliken ‘C/C++’. Välj kategori ‘Language’ och markera ‘Enable Exception Handling’. Standardinställningen har switchen ‘/GX-’ aktiverad.

 


Syntax.

 

Nedan följer ett syntaxdiagram:

 

Syntax för ett försöksblock ‘try-block’:

 

try

{

    <programsatser och ‘throw’-satser>...

}

 

Här är programsatserna den ‘skyddade’ koden, i vilken ‘throw’-satser vävts in på de ställen där man vill göra kontroller.

 

Syntax för ‘throw’-sats:

 

throw [<uttryck>|<typ>];

 

Lägg märke till att man använder samma syntax som för en ‘return’. Det är som synes i syntaxschemat tillåtet att utesluta <uttryck>, men som vi kommer att se i ett senare avsnitt är detta tillåtet om ‘throw’ satsen står i ett ‘catch’-avsnitt. Det är även tillåtet att bara ange en datatyp. Detta gör att hanteraren inte får något ar­gument, men att rätt hanterare kan identifieras m.h.a. datatypen.

 

Direkt efter ‘try’-avsnittet kommer en eller flera hanterare. Dessa har följande syn­tax:

 

catch (<undantagsdeklaration>) <felhanteringskod>

 

undantagsdeklaration:
   <typdeklaration>
   <abstrakt typdeklaration >
   <typdeklarationslista>
   ...

 

Kodavsnittet efter nyckelordet ‘try’ är den skyddade koden. I denna ingår de ‘throw’-satser man väver in för att utlösa undantagshanteringen på de ställen där man vill ha kontroller. Observera att många av de funktioner som ingår i API (Application Programming Interface) själva kastar ut undantag utan att fånga upp dem. Dessa undantag kan vi följa upp, och de står beskrivna i hjäl­pen.

 

Undantagsdeklarationen i ‘catch’-satsen deklarerar vilken typ av undantag sat­sen hanterar. Det kan vara vilken korrekt datayp som helst, inklusive en C++ klass. Om matchande undantag har kastats ut kommer felhanteringaskoden efter ‘catch’-deklarationen att utföras. En ‘catch’-sats består alltså av en ‘catch’-dek­la­­ration (inkluderande ‘catch’-typ i undantagsdeklarationen) samt ett kodav­snitt, felhanteringskoden.

 

Undantagsdeklarationen kan innehålla ‘any type’, d.v.s. undantag av valfri typ. Detta anges med tre punkter: ‘catch (...)’. Vid en sådan deklaration kan ‘catch’-satsen hantera alla sorters undantag, inklusive C-undantag, systemgenererade undantag och applikationsspecifika undantag. Detta inkluderar undantag av ty­p­en minnesskydd, division med noll och brott mot flyttalshanteringen. Vill man kom­binera detta med speciell hantering av vissa typer måste den allmänna ‘catch’-satsen stå sist, annars fångar den upp det specifika undantaget innan dess egentliga ‘catch’-sats bearbetats, och man kommer aldrig dit.

 

 


Exempel.

 

Vi kan testa med att lägga till hantering av fel när vi misslyckas med att allo­k­era minne:

 

#include <iostream.h>

#include <string.h>

 

int main()

{

    char *buf;

    try

    {

        buf = new char[512];

        if(buf == 0)

        {

            throw "Misslyckades att reservera minne!";

        }

        // Använd det reserverade minnet:

        strcpy(buf, ”Exempeltext”);

        ...

    }

    catch(char * str)

    {

        cout << "Ett fel uppstod: " << str << ‘\n’;

    }

    // Fortsatt bearbetning, om allt OK:

    ...

    return 0;

}

 

Om minnesallokeringen går bra kommer koden under kommentaren ‘// Använd det reserverade minnet:’ att utföras, och ‘catch’-satsen hoppas sedan över. Om ‘new’ returnerar noll, d.v.s. det gick inte att reservera minne, kommer koden ef­ter kommerntaren inte att utföras, men väl ‘catch’-satsen.

 

Operand till ‘throw’ är en char*, vilket innebär att den felet kan fångas upp av en ‘catch’ som tar en char* som argument. Observera att den texten skrivs ut till­sammans med ett generellt meddelande i ‘catch’-kodavsnittet. Om fel uppstår kommer utskriften att bli:

 

Ett fel uppstod: Misslyckades att reservera minne!

 

Observera att det inte går att styra programlödet till det kodavsnitt som tillhör en ‘catch’-sats på annat sätt än via ett undantag. Det går alltså inte med t.ex. ‘goto’.

 

 


Återställning.

 

Vi har redan sett fördelen att man kan skilja undantagen från varandra genom dess typ, men den stora fördelen i C++ undantagshantering är förmågan att åter­ställa ursprunglig situation, d.v.s. den situation som rådde precis före ett ‘try’-avsnitt som avbrutits av att ett undantag utlösts. Detta kallas i Microsofts texter för ‘unwind’. En mer korrekt översättning vore kanske ‘nysta upp avbrutet ske­ende’, men ‘återställa’ räcker gott.

 

Det sammanhang som råder mellan ett ‘throw’ och dess motsvarande ‘catch’ kal­las ‘exception stack frame’, ung. undantagets buffertutrymme. Detta utrym­me kan eventuellt innehålla objekt vilka har viktig städning i sina destruktorer. Om ett undantag skulle uppstå inom den skyddade koden, eller i en funktion kal­lad av den skyddade koden, kommer ett undantagsobjekt att skapas av det objekt som skapas av ‘thow’-operanden. (Detta innebär att det kan komma att involvera en ‘copy constructor’.) I detta läge söker kompilatorn en ‘catch’ i ett högre sammanhang som kan hantera undantag av denna typ, eller en som kan hantera valfri typ. De olika ‘catch’-satserna undersöks i den ordning de står ef­ter ‘try’-avsnittet. Om ingen passande ‘catch’ finns genomsöks nästa över­grip­ande sammanhang, d.v.s. omgivande ‘try’-avsnitts undantagshanterare. Detta fortsätter tills ingen övergripande nivå av ‘try’-avsnitt återfinnes.

 

Om passande undantagshantering ändå inte återfanns, eller om ett nytt undantag uppstår under återställningsarbetet innan hanteraren får kontrollen, kommer den fördefinierade runtimefunktionen ‘terminate’ att anropas. Detta gäller även om återställningsarbetet inte hunnit påbörjas när nytt undantag uppstår.

 

Man kan som sagt skapa en egen hantering för ‘terminate’, för att hantera dylika situationer.

 

Här nedan finns ett exempel på C++ undantagshantering hanterar återställning där klasser med ‘städning’ i destruktorn förekommer. För tydlighets skull har städningen symboliserats med texter, så att vi kan följa skeendet i utskriften. I exemplet skapas två klasser: en klass ‘CTest’ definierar den typ undantaget ska hantera, och en klass ‘CDtorDemo’ vilken visar hur ett separat objekt förstörs korrekt under städningen.

 

#include <iostream.h>

 

void MyFunc( void );

 

class CTest

{

public:

    CTest(){};

    ~CTest(){};

    const char *ShowReason() const {return

                             "Exception in CTest class.";}

};

 

class CDtorDemo

{

public:

    CDtorDemo();

    ~CDtorDemo();

};

 

CDtorDemo::CDtorDemo()

{

    cout << "Constructing CDtorDemo." << endl;

}

 

CDtorDemo::~CDtorDemo()

{

    cout << "Destructing CDtorDemo." << endl;

}

 

void MyFunc()

{

    CDtorDemo D;

    cout<< "In MyFunc(). Throwing CTest exception." << endl;

    throw CTest();

}

 

int main()

{

    cout << "In main." << endl;

    try

    {

        cout << "In try block, calling MyFunc()." << endl;

        MyFunc();

    }

    catch( CTest E )

    {

        cout << "In catch handler." << endl;

        cout << "Caught CTest exception type: ";

        cout << E.ShowReason() << endl;

    }

    catch( char *str )

    {

        cout << "Caught some other exception: " << str << endl;

    }

    cout << "Back in main. Execution resumes here." << endl;

    return 0;

}

 

Om en matchande ‘catch’ finns, och denna tar emot argument (inte bara typ) kom­mer detta argument att initieras med en kopia på undantagsobjektet (det som skapades vid ‘throw’). Om det tar en referens till ett objekt kommer argu­mentet att initieras som referens till undantagsobjektet. När argumentet väl är initierat kan själva städningen börja. Nu ska alla automatiskt skapade objekt de­s­trueras, samt övriga objekt som skapats i ‘try’-avsnittet (utom de som eventu­ellt redan destruerats). Objekten destrueras i omvänd ordning mot den i vilken de skapades. Sedan körs ‘catch’-avsnittet för den matchande ‘catch’-en, och till sist kommer vi till den kod som står efter sista ‘catch’-avsnitt. Observera att en­dast ett ‘catch’-avsnitt körs. Om det finns flera ‘catch’-deklarationer som mat­ch­ar undantagets typ kommer den först påträffade att köras, och inga andra. Det är därför det är viktigt att en allmän ‘catch’ står sist, d.v.s. ‘catch(...)’ ska stå sist om det förekommer en sådan.

 

Så här blir utskriften från exemplet ovan:

 

In main.

In try block, calling MyFunc().

Constructing CDtorDemo.

In MyFunc(). Throwing CTest exception.

Destructing CDtorDemo.

In catch handler.

Caught CTest exception type: Exception in CTest class.

Back in main. Execution resumes here.

 

Observera att det inte är nödvändigt att deklarera argument till en ‘catch’-deklaration, det räcker med en datatyp om man bara behöver ange vilken datatyp som undantaget hör, och inte tänker använda argumentet. I ovanstående exempel har vi dock använt oss av CTest-objektet ‘E’ och textpekaren ‘*str’. 

 

Ett ‘throw’-uttryck utan operand kommer att vidarepassa samma argument som den själv fick. Detta innebär naturligtvis att ett sådant endast kan stå i ett annat ‘throw’-uttryck, eller i en funktion som anropas från ett dylikt. Det är samma ob­jekt som passeras vidare, inte en kopia. Betrakta nedanstående exempel:

 

try

{

    throw CSomeOtherException();

}

catch(...)      // Handle all exceptions

{

    // Respond (perhaps only partially) to exception

    //...

 

    throw;      // Pass exception to some other handler

}

 

 


Identifiering av undantagstyp.

 

Eftersom C++ accepterar att man kastar ut undantag av valfri typ är det viktigt att man vet vilka typer som känns igen av vilka ‘catch’-deklarationer. För det första kan ett undantag fångas upp av en hanterare med samma typ, en som tar en referens till samma typ, eller en som tar alla typer.

 

Om typen är en klass, vilken ärver av en eller flera klasser kan den kännas igen av en hanterare som är avsedd för förälder/bas-klassens typ, såväl som referens till dito. Observera att vid referens är hanteraren bunden till originalet, vilket na­turligtvis annars inte är fallet eftersom objektet automatiskt kopieras.

 

Ett undantag av viss typ kan hanteras av följande hanterare:

 

·     En hanterare avsedd för valfri typ ‘catch(...)’.

·     En hanterare av samma typ som undantaget avser. Nyckelorden ‘const’ och ‘volatile’ ignoreras, eftersom det objekt hanteraren eventuellt tar emot blir en kopia på orginalobjektet.

·     En hanterare som tar en referens till ett objekt av samma typ som undantaget avser.

·     En hanterare som tar en referens till ett ‘const’ eller ‘volatile’ objekt av samma typ som undantaget avser.

·     En hanterare som tar en typ som fungerar som förälder/bas-klass till samma typ som undantaget avser. Nyckelorden ‘const’ och ‘volatile’ ignoreras, eftersom det objekt hanteraren eventuellt tar emot blir en kopia på orginalobjektet.

·     En hanterare som tar en referens till ett objekt av en typ som fungerar som förälder/bas-klass till samma typ som undantaget avser.

·     En hanterare som tar en referens till ett ‘const’ eller ‘volatile’ objekt av en typ som fungerar som förälder/bas-klass till samma typ som undantaget avser.

·     En hanterare som tar en pekare mot ett objekt av samma typ som undantaget avser, eller en pekare av en typ som kan typomvandlas enligt standardregler till att uppfylla detta.

 

 


Undantag som inte hanterats.

 

Om en passande hanterare inte påträffats enligt ovanstående regler, varken på samma nivå som undantaget inträffat eller någon övergripande nivå, kommer alltså den fördefinierade funktionen ‘terminate’ att anropats. Man kan även själv anropa ‘terminate’ i valfri hanterare. I originalversionen kommer ‘termi­nate’ att be operativsystemet att avbryta programmet (abort). Man kan definiera om detta beteende genom att anropa ‘set_terminate()’, vilken tar namnet på den egendefinierade funktionen som enda argument. Man kan anropa set_termin­a­te() var som helt i sitt program. Om terminate behöver anropas kommer den att välja den funktion man senast angav via set_terminate, om man gjort detta.

 

Nedanstående exempel kastar ut ett char*-undantag, för vilket det i detta fallet inte finns någon hanterare. Ett anrop till set_terminate() anger att undantaget ska anropa en funktion vid namn ‘term_func()’:

 

#include <eh.h>      // For function prototypes

#include <iostream.h>

#include <process.h>

 

void term_func()

{

    //...

    cout << "term_func was called by terminate." << endl;

    exit( -1 );

 }

int main()

{

    try

    {

        // ...

        set_terminate( term_func );

        // ...

        throw "Shit happens!"; // No catch handler for this

                               // exception

    }

    catch( int )

    {

        cout << "Integer exception raised." << endl;

    }

    return 0;

}

 

När en egendefinierad terminate är klar med det man vill att den ska göra bör man avsluta programmet genom att anropa ‘exit()’. Om man inte gör det kom­mer abort() automatiskt att anropas.